﻿using System.Collections.Generic;
using System.Linq;
using DivaScript;

namespace ScriptUtilities
{
    public static class CommandFormatting
    {
        // TARGET - FT:
        // [0] target_type
        // [1] x_position
        // [2] y_position
        // [3] button_angle
        // [4] button_distance
        // [5] button_amplitude
        // [6] button_frequency

        // TARGET - F:
        // [0] target_type
        // [1] hold_duration
        // [2] hold_end_flag
        // [3] x_position
        // [4] y_position
        // [5] button_angle
        // [6] button_frequency
        // [7] button_distance
        // [8] button_amplitude
        // [9] button_ms_fly_duration
        // [A] time_signature
        // [B] end_unk

        public static List<PvCommand> AddTargetFlyingTimeCommands(List<PvCommand> commands)
        {
            for (int i = 0; i < commands.Count; i++)
            {
                var command = commands[i];
                if (command.Opcode.ToUpper() == "TARGET" && command.Parameters.Count > 9)
                {
                    int flyDuration = command.Parameters[9];

                    if (flyDuration > 0)
                    {
                        commands.Insert(i, new PvCommand(command.TimeStamp, "TARGET_FLYING_TIME", new List<int> { flyDuration }));
                        i++;
                    }
                }
            }

            return commands;
        }

        public static List<PvCommand> ReformatFTargetList(List<PvCommand> pvCommands)
        {
            pvCommands.Where(c => c.Opcode.ToUpper() == "TARGET").ToList().ForEach(c => c = FtoFtTarget(c));
            return pvCommands;
        }

        public static List<PvCommand> ReformatFtTargetList(List<PvCommand> pvCommands)
        {
            pvCommands.Where(c => c.Opcode.ToUpper() == "TARGET").ToList().ForEach(c => c = FtToFTarget(c));
            return pvCommands;
        }

        public static PvCommand FtoFtTarget(PvCommand fTargetCommand)
        {
            if (fTargetCommand.Parameters.Count < 9)
                return fTargetCommand;

            List<int> oldParam = fTargetCommand.Parameters;

            int target_type = oldParam[0];
            int x_position = oldParam[3];
            int y_position = oldParam[4];
            int button_angle = oldParam[5];
            int button_distance = oldParam[7];
            int button_amplitude = oldParam[8];
            int button_frequency = oldParam[6];

            var newParam = new List<int>()
                {
                    target_type,
                    x_position,
                    y_position,
                    button_angle,
                    button_distance,
                    button_amplitude,
                    button_frequency,
                };

            fTargetCommand.Parameters = newParam;
            return fTargetCommand;
        }

        public static PvCommand FtToFTarget(PvCommand ftTargetCommand)
        {
            if (ftTargetCommand.Parameters.Count < 7)
                return ftTargetCommand;

            List<int> oldParam = ftTargetCommand.Parameters;

            int target_type = oldParam[0];
            int hold_duration = -1;
            int hold_end_flag = -1;
            int x_position = oldParam[1];
            int y_position = oldParam[2];
            int button_angle = oldParam[3];
            int button_frequency = oldParam[6];
            int button_distance = oldParam[4];
            int button_amplitude = oldParam[5];
            int button_ms_fly_duration = 0;
            int time_signature = 3;
            int end_unk = -1;

            var newParam = new List<int>()
            {
                target_type,
                hold_duration,
                hold_end_flag,
                x_position,
                y_position,
                button_angle,
                button_frequency,
                button_distance,
                button_amplitude,
                button_ms_fly_duration,
                time_signature,
                end_unk,
            };
            ftTargetCommand.Parameters = newParam;
            return ftTargetCommand;
        }

        public static List<PvCommand> AdjustTargetTypes(List<PvCommand> commands, params (int inputType, int outputType)[] formats)
        {
            var targets = commands.Where(c => c.Opcode.ToUpper() == "TARGET");

            foreach (var target in targets)
            {
                if (target.Parameters.Count < 1)
                    continue;

                foreach (var (inputType, outputType) in formats)
                {
                    AdjustTargetType(target, inputType, outputType);
                }
            }

            return commands;
        }

        public static PvCommand AdjustTargetType(PvCommand command, int inputType, int outputType)
        {
            if (command.Parameters[0] == inputType)
            {
                command.Parameters[0] = outputType;
            }

            return command;
        }

        public static List<PvCommand> SmartReformatFtoFt(List<PvCommand> pvCommands)
        {
            pvCommands = AddTargetFlyingTimeCommands(pvCommands);

            foreach (var command in pvCommands.ToList())
            {
                if (command.Opcode.ToUpper() == "TARGET")
                {
                    if (command.Parameters[2] > -1) // is hold end
                    {
                        pvCommands.Remove(command);
                    }
                    else
                    {
                        FtoFtTarget(command);
                    }
                }
            }

            AdjustTargetTypes(pvCommands, new (int inputType, int outputType)[]
            {
                ((int)F2TargetType.sankaku_w, (int)FtTargetType.sankaku),
                ((int)F2TargetType.maru_w, (int)FtTargetType.maru),
                ((int)F2TargetType.batsu_w, (int)FtTargetType.batsu),
                ((int)F2TargetType.shikaku_w, (int)FtTargetType.shikaku),

                ((int)F2TargetType.sankaku_long, (int)FtTargetType.sankaku_hold),
                ((int)F2TargetType.maru_long, (int)FtTargetType.maru_hold),
                ((int)F2TargetType.batsu_long, (int)FtTargetType.batsu_hold),
                ((int)F2TargetType.shikaku_long, (int)FtTargetType.shikaku_hold),

                ((int)F2TargetType.touch, (int)FtTargetType.slide_r),
                ((int)F2TargetType.touch_link, (int)FtTargetType.slide_l),
                ((int)F2TargetType.touch_link_end, (int)FtTargetType.slide_r),
                ((int)F2TargetType.touch_w, (int)FtTargetType.slide_r),
                ((int)F2TargetType.touch_ch0, (int)FtTargetType.slide_l_ch),
                ((int)F2TargetType.touch_ch1, (int)FtTargetType.slide_r_ch),

                ((int)F2TargetType.sankaku_rush, (int)FtTargetType.sankaku_hold),
                ((int)F2TargetType.maru_rush, (int)FtTargetType.maru_hold),
                ((int)F2TargetType.batsu_rush, (int)FtTargetType.batsu_hold),
                ((int)F2TargetType.shikaku_rush, (int)FtTargetType.shikaku_hold),
                ((int)F2TargetType.touch_rush, (int)FtTargetType.maru_hold),
            });

            return pvCommands;
        }

        public static List<PvCommand> SmartReformatFtToF(List<PvCommand> pvCommands, bool touchHold = false)
        {
            List<PvCommand> originalList = new List<PvCommand>(pvCommands);

            // create target structs
            List<FtTargetStruct> chainTargetStructsLeft = new List<FtTargetStruct>();
            List<FtTargetStruct> chainTargetStructsRight = new List<FtTargetStruct>();

            {
                int flyDuration = 0;

                for (int i = 0; i < originalList.Count; i++)
                {
                    var target = originalList[i];

                    if (target.Opcode == "TARGET_FLYING_TIME")
                    {
                        flyDuration = target.Parameters.FirstOrDefault();
                    }
                    else if (target.Opcode == "TARGET")
                    {
                        var targetStruct = new FtTargetStruct(target, flyDuration);

                        if (targetStruct.TargetType == FtTargetType.slide_chain_l)
                        {
                            chainTargetStructsLeft.Add(targetStruct);
                        }
                        else if (targetStruct.TargetType == FtTargetType.slide_chain_r)
                        {
                            chainTargetStructsRight.Add(targetStruct);
                        }
                    }
                }

                { // update chains

                    UpdateChainList(chainTargetStructsLeft);
                    UpdateChainList(chainTargetStructsRight);

                    void UpdateChainList(List<FtTargetStruct> targets)
                    {
                        List<(float, FtTargetStruct)> differences = new List<(float, FtTargetStruct)>();

                        float prevDiff = 0;

                        for (int i = 0; i < targets.Count - 1; i++)
                        {
                            FtTargetStruct prevFragment = (i > 0) ? targets[i - 1] : null;
                            FtTargetStruct thisFragment = targets[i];
                            FtTargetStruct nextFragment = (targets.Count + 1 > i) ? targets[i + 1] : null;

                            float diff = thisFragment.FlyDuration / (float)(nextFragment.TimeStamp - thisFragment.TimeStamp);

                            if (true)
                                differences.Add((diff, thisFragment));

                            if (prevFragment == null)
                            {
                                thisFragment.ChainStart = true;
                            }
                            else
                            {
                                const float THRESHOLD = .2f;

                                bool isStart = (diff > prevDiff && (!Helper.IsInRange((diff - prevDiff), THRESHOLD)));
                                nextFragment.ChainStart = isStart;

                                if (isStart)
                                    thisFragment.ChainEnd = true;
                            }
                            prevDiff = diff;
                        }
                        if ((targets.Count > 1) && (!targets[targets.Count - 1].ChainEnd))
                            targets[targets.Count - 1].ChainEnd = true;
                    }
                }
            }

            {
                int flyDuration = 0;

                foreach (var command in pvCommands.ToList())
                {
                    if (command.Opcode.ToUpper() == "TARGET_FLYING_TIME")
                        flyDuration = command.Opcode.FirstOrDefault();

                    if (command.Opcode.ToUpper() == "TARGET")
                    {
                        FtToFTarget(command);

                        bool isSync = pvCommands.Exists(t => t.TimeStamp == command.TimeStamp && t.Opcode == command.Opcode && t != command);

                        switch ((FtTargetType)command.Parameters[0])
                        {
                            case FtTargetType.sankaku:
                                command.Parameters[0] = (int)F2TargetType.sankaku;
                                break;

                            case FtTargetType.maru:
                                command.Parameters[0] = (int)F2TargetType.maru;
                                break;

                            case FtTargetType.batsu:
                                command.Parameters[0] = (int)F2TargetType.batsu;
                                break;

                            case FtTargetType.shikaku:
                                command.Parameters[0] = (int)F2TargetType.shikaku;
                                break;

                            case FtTargetType.sankaku_hold:
                                command.Parameters[0] = (int)(isSync ? F2TargetType.sankaku : F2TargetType.sankaku_w);
                                break;

                            case FtTargetType.maru_hold:
                                command.Parameters[0] = (int)(isSync ? F2TargetType.maru : F2TargetType.maru_w);
                                break;

                            case FtTargetType.batsu_hold:
                                command.Parameters[0] = (int)(isSync ? F2TargetType.batsu : F2TargetType.batsu_w);
                                break;

                            case FtTargetType.shikaku_hold:
                                command.Parameters[0] = (int)(isSync ? F2TargetType.shikaku : F2TargetType.shikaku_w);
                                break;

                            case FtTargetType.slide_l:
                            case FtTargetType.slide_r:
                                command.Parameters[0] = (int)F2TargetType.touch;
                                break;

                            case FtTargetType.slide_chain_l:
                            case FtTargetType.slide_chain_r:
                                pvCommands.Remove(command);
                                break;

                            case FtTargetType.sankaku_ch:
                            case FtTargetType.maru_ch:
                            case FtTargetType.batsu_ch:
                            case FtTargetType.shikaku_ch:
                                command.Parameters[0] = (int)F2TargetType.touch_ch1;
                                break;

                            case FtTargetType.slide_l_ch:
                            case FtTargetType.slide_r_ch:
                                command.Parameters[0] = (int)F2TargetType.touch_ch1;
                                break;

                            default:
                                break;
                        }
                    }
                }

                ChainFragmentListToLongTargets(chainTargetStructsLeft);
                ChainFragmentListToLongTargets(chainTargetStructsRight);

                void ChainFragmentListToLongTargets(List<FtTargetStruct> targets)
                {
                    foreach (var target in targets.ToList())
                    {
                        if (target.ChainStart)
                        {
                            PvCommand command = target.OriginalCommand;

                            bool left = target.TargetType == FtTargetType.slide_chain_l;

                            command.Parameters[0] = (int)(left ? F2TargetType.shikaku_long : F2TargetType.maru_long);

                            if (touchHold)
                            {
                                command.Parameters[0] = 13;
                            }

                            int chainDuration = 0;
                            int lastTime = 0;
                            foreach (var fragment in targets.ToList())
                            {
                                bool shouldBreak = false;

                                if (fragment.ChainEnd)
                                {
                                    chainDuration = lastTime - target.TimeStamp;
                                    shouldBreak = true;
                                }

                                lastTime = fragment.TimeStamp;
                                targets.Remove(fragment);

                                if (shouldBreak)
                                    break;
                            }

                            command.Parameters[1] = chainDuration;

                            pvCommands.Add(new PvCommand(command.TimeStamp, "TIME", command.TimeStamp));
                            pvCommands.Add(command);

                            int endTime = command.TimeStamp + chainDuration;
                            pvCommands.Add(new PvCommand(endTime, "TIME", endTime));

                            pvCommands.Add(new PvCommand(endTime, "TARGET",
                                  command.Parameters[0],
                                 -1,
                                 +1,
                                 command.Parameters[3],
                                 command.Parameters[4],
                                 command.Parameters[5],
                                 command.Parameters[6],
                                 command.Parameters[7],
                                 command.Parameters[8],
                                 command.Parameters[9],
                                 command.Parameters[10]));
                        }
                    }
                }
            }

            pvCommands = pvCommands.Where(c => c.Opcode.ToUpper() != "TIME").ToList();
            pvCommands = PvScript.OrderListByTime(PvScript.AddTimeCommands(pvCommands));

            return pvCommands;
        }

        private class FtTargetStruct
        {
            public PvCommand OriginalCommand = null;

            public bool ChainStart = false;

            public bool ChainEnd = false;

            public int TimeStamp = -1;

            public int FlyDuration = -1;

            public FtTargetType TargetType;

            public FtTargetStruct(PvCommand command, int flyDuration)
            {
                OriginalCommand = command;

                TimeStamp = command.TimeStamp;

                FlyDuration = flyDuration;

                TargetType = (FtTargetType)command.Parameters.FirstOrDefault();
            }
        }
    }
}
